---
title: 单点登录
---
单点登录，即一次登录，到处访问。比如一个集团下有十个产品，只需要在其中的一个产品登录后其它产品都可以直接进入。

不管那种方式的登录，都有两个过程，登录和对登录状态的校验，单点登录也是包含这两个过程的。

## 单点登录的实现方式

单点登录有两种方式：分别是 redis + token 和 jwt。

对于 redis + token 的方法，这个 token 是一个随机的字符串，为用户每次登录时生成的，而且这个 token 不能使用用户id。如果使用用户id，每个用户登录生成的 token 都是固定的，容易被破解。所以每个用户每次登录 token 都应该是不一样的，是一个无意义的、随机的字符串。

通常将 token 作为 key ，用户的信息作为 value 存入到 Redis 中，再把这个 token 返回给前端，前端后续的每个请求都把这个 token 给带上，这样就实现了单点登录。

下面是 token + redis 单点登录方式的流程图。

1. 每次都要生成随机的 token ，然后要将 token 放到 Reids，前端需要保存 token ，以后每次请求都将 token 放到 header 里面。
2. 校验时，需要从 Redis 里面获取 Token 。

![image-20230331220808141](https://img.wkq.pub/pic/image-20230331220808141.png)

对于 JWT 方式，它可以不使用 Redis ，因为 JWT 它生成的是一个有意义的 Token （用户信息的加密数据），通过加密数据就可以反向解密出当前登录的是哪一个用户。最终的目的都是为了校验出用户是否登录。

下面是 JWT 单点登录的流程图：

![image-20230331221849502](https://img.wkq.pub/pic/image-20230331221849502.png)

## JWT 原理

JWT 是一种网络身份认证和信息交换格式。分为三部分：
1. Header 头部信息，主要声明了 JWT 的签名算法等信息。
2. Payload 载荷信息，主要承载了各种声明并传递明文数据。
3. Signature 签名，拥有该部分的 JWT 被称为 JWS，也就是签了名的 JWS，用于校验数据。

```java title='整体结构是：'
header.payload.signature
```
使用 JWT 主要包括以下三个方法：
1. JWT 创建
2. JWT 解析
3. JWT 验证
```xml title='引入依赖'
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.18.1</version>
</dependency>
```

```java title='一个用于生成、解析和验证JWT Token的Java工具类'
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import java.security.Key;
import java.util.Date;

public class JwtUtils {

    // 用于签名JWT的密钥
    private static final Key SIGNING_KEY = Keys.secretKeyFor(SignatureAlgorithm.HS256);

    // JWT的有效期
    private static final long EXPIRATION_TIME = 3600_000; // 1小时

    /**
     * 生成JWT token
     *
     * @param subject JWT中的主题，可以是任意字符串
     * @return 生成的JWT token
     */
    public static String generateToken(String subject) {
        Date now = new Date();
        Date expiration = new Date(now.getTime() + EXPIRATION_TIME);

        return Jwts.builder()
                .setSubject(subject)
                .setIssuedAt(now)
                .setExpiration(expiration)
                .signWith(SIGNING_KEY)
                .compact();
    }

    /**
     * 解析JWT token
     *
     * @param token 要解析的JWT token
     * @return 解析出来的JWT中的数据
     * @throws Exception 如果解析失败，则抛出异常
     */
    public static Claims parseToken(String token) throws Exception {
        return Jwts.parserBuilder()
                .setSigningKey(SIGNING_KEY)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    /**
     * 验证JWT token是否合法
     *
     * @param token 要验证的JWT token
     * @param subject 期望的主题
     * @return 如果token合法，返回true；否则返回false
     */
    public static boolean validateToken(String token, String subject) {
        try {
            Claims claims = parseToken(token);
            return claims.getSubject().equals(subject);
        } catch (Exception e) {
            return false;
        }
    }
}
```
```java title='使用该工具类生成JWT token的示例代码：'
String token = JwtUtils.generateToken("example");
System.out.println(token);
```
```java title='使用该工具类解析JWT token的示例代码：'
String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJleGFtcGxlIiwiaWF0IjoxNjE3MTM3MjA3LCJleHAiOjE2MTcxNDA4MDd9.A6RkW8JnBj9A5f5p5N5JW8VvZgGnE7VH5IzxlW1liBo";
Claims claims = JwtUtils.parseToken(token);
System.out.println(claims.getSubject());
```

## JWT 存在的问题及解决方案
* token 被解密，需要加盐值(密钥)，每个项目的盐值都不一样。
* token 被拿到第三方使用，使用限流方法解决。



